import {
	PersistedFairyAgentState,
	PersistedFairyConfigs,
	PersistedFairyState,
} from '@tldraw/fairy-shared'
import { useCallback, useEffect, useRef } from 'react'
import { react, throttle, useEditor, useToasts, useValue } from 'tldraw'
import { useApp } from '../tla/hooks/useAppState'
import { useTldrawUser } from '../tla/hooks/useUser'
import { FairyAgent } from './fairy-agent/agent/FairyAgent'
import { $fairyProjects } from './FairyProjects'
import { FairyTaskDragTool } from './FairyTaskDragTool'
import { $fairyTasks, $showCanvasFairyTasks } from './FairyTaskList'
import { FairyThrowTool } from './FairyThrowTool'

export function FairyApp({
	setAgents,
	fileId,
}: {
	setAgents(agents: FairyAgent[]): void
	fileId: string
}) {
	const editor = useEditor()
	const user = useTldrawUser()
	const app = useApp()
	const toasts = useToasts()
	const fairyConfigs = useValue(
		'fairyConfigs',
		() => JSON.parse(app?.getUser().fairies || '{}') as PersistedFairyConfigs,
		[app]
	)

	const getToken = useCallback(async () => {
		if (!user) return undefined
		return await user.getToken()
	}, [user])

	const handleError = useCallback(
		(e: any) => {
			const message = typeof e === 'string' ? e : e instanceof Error && e.message
			toasts.addToast({
				title: 'Error',
				description: message || 'An error occurred',
				severity: 'error',
			})
			console.error(e)
		},
		[toasts]
	)

	// Track whether we're currently loading state to prevent premature saves
	const isLoadingStateRef = useRef(false)
	// Keep a ref to agents so effects can access them
	const agentsRef = useRef<FairyAgent[]>([])
	// Track which agents have been loaded to avoid reloading existing agents
	const loadedAgentIdsRef = useRef<Set<string>>(new Set())
	const fairyTaskListLoadedRef = useRef(false)
	const showCanvasTodosLoadedRef = useRef(false)
	const projectsLoadedRef = useRef(false)

	// Create agents dynamically from configs
	useEffect(() => {
		if (!editor) return

		// Register the FairyThrowTool
		const selectTool = editor.root.children!.select
		editor.removeTool(FairyThrowTool, selectTool)
		editor.setTool(FairyThrowTool, selectTool)

		// Register the TodoDragTool
		editor.removeTool(FairyTaskDragTool, selectTool)
		editor.setTool(FairyTaskDragTool, selectTool)

		const configIds = Object.keys(fairyConfigs)
		const existingAgents = agentsRef.current
		const existingIds = new Set(existingAgents.map((a) => a.id))

		// Find agents to create (new configs that don't have agents yet)
		const idsToCreate = configIds.filter((id) => !existingIds.has(id))

		// Find agents to dispose (agents that no longer have configs)
		const configIdsSet = new Set(configIds)
		const agentsToDispose = existingAgents.filter((agent) => !configIdsSet.has(agent.id))

		// Dispose removed agents and clean up tracking
		agentsToDispose.forEach((agent) => {
			agent.dispose()
			loadedAgentIdsRef.current.delete(agent.id)
		})

		// Create new agents
		const newAgents = idsToCreate.map((id) => {
			return new FairyAgent({
				id,
				app,
				editor,
				onError: handleError,
				getToken,
			})
		})

		// Keep existing agents that are still in config, add new ones
		const updatedAgents = [
			...existingAgents.filter((agent) => configIdsSet.has(agent.id)),
			...newAgents,
		]

		agentsRef.current = updatedAgents
		setAgents(updatedAgents)
	}, [fairyConfigs, editor, getToken, handleError, setAgents, app])

	// Cleanup: dispose all agents only when component unmounts
	useEffect(() => {
		return () => {
			agentsRef.current.forEach((agent) => agent.dispose())
		}
	}, [])

	// Load fairy state from backend when agents are created
	useEffect(() => {
		if (!app || agentsRef.current.length === 0 || !$fairyTasks || !$showCanvasFairyTasks || !fileId)
			return

		const fileState = app.getFileState(fileId)
		if (fileState?.fairyState) {
			try {
				const fairyState: PersistedFairyState = JSON.parse(fileState.fairyState)

				// Mark that we're loading to prevent save watchers from firing
				isLoadingStateRef.current = true

				// Only restore state for agents that haven't been loaded yet
				agentsRef.current.forEach((agent) => {
					// Skip if already loaded
					if (loadedAgentIdsRef.current.has(agent.id)) return

					const persistedAgent = fairyState.agents[agent.id]
					if (persistedAgent) {
						agent.loadState(persistedAgent)
						loadedAgentIdsRef.current.add(agent.id)
					}
				})

				// Load shared todo list only once
				if (fairyState.fairyTaskList && !fairyTaskListLoadedRef.current) {
					$fairyTasks.set(fairyState.fairyTaskList)
					fairyTaskListLoadedRef.current = true
				}

				// Load show canvas todos only once
				if (fairyState.showCanvasTodos && !showCanvasTodosLoadedRef.current) {
					$showCanvasFairyTasks.set(fairyState.showCanvasTodos)
					showCanvasTodosLoadedRef.current = true
				}

				// Load projects only once
				if (fairyState.projects && !projectsLoadedRef.current) {
					$fairyProjects.set(fairyState.projects)
					projectsLoadedRef.current = true
				}

				// Allow a tick for state to settle before allowing saves
				setTimeout(() => {
					isLoadingStateRef.current = false
				}, 100)
			} catch (e) {
				console.error('Failed to load fairy state:', e)
				isLoadingStateRef.current = false
			}
		}
	}, [app, fairyConfigs, fileId])

	// Todo: Use FileStateUpdater for this
	// Save fairy state to backend periodically
	useEffect(() => {
		if (!app || agentsRef.current.length === 0 || !fileId) return

		const updateFairyState = throttle(() => {
			// Don't save if we're currently loading state
			if (isLoadingStateRef.current) return

			const fairyState: PersistedFairyState = {
				agents: agentsRef.current.reduce(
					(acc, agent) => {
						acc[agent.id] = agent.serializeState()
						return acc
					},
					{} as Record<string, PersistedFairyAgentState>
				),
				fairyTaskList: $fairyTasks.get(),
				showCanvasTodos: $showCanvasFairyTasks.get(),
				projects: $fairyProjects.get(),
			}
			app.onFairyStateUpdate(fileId, fairyState)
		}, 2000) // Save maximum every 2 seconds

		// Watch for changes in fairy atoms
		const fairyCleanupFns: (() => void)[] = []
		agentsRef.current.forEach((agent) => {
			const cleanup = react(`${agent.id} state`, () => {
				agent.$fairyEntity.get()
				agent.$chatHistory.get()
				agent.$personalTodoList.get()
				updateFairyState()
			})
			fairyCleanupFns.push(cleanup)
		})

		const cleanupSharedFairyState = react('shared fairy atom state', () => {
			$fairyTasks.get()
			$showCanvasFairyTasks.get()
			$fairyProjects.get()
			updateFairyState()
		})

		return () => {
			updateFairyState.flush()
			cleanupSharedFairyState()
			fairyCleanupFns.forEach((cleanup) => cleanup())
		}
	}, [app, fairyConfigs, fileId])

	return null
}
